001    /*
002     * Copyright 2004-2005 Stephen McConnell
003     *
004     * Licensed  under the  Apache License,  Version 2.0  (the "License");
005     * you may not use  this file  except in  compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     *   http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed  under the  License is distributed on an "AS IS" BASIS,
012     * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY KIND, either  express  or
013     * implied.
014     *
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    
019    package net.dpml.tools.tasks;
020    
021    import java.io.File;
022    import java.io.IOException;
023    import java.io.InputStream;
024    import java.io.FileInputStream;
025    import java.util.Enumeration;
026    import java.util.Properties;
027    import java.util.StringTokenizer;
028    
029    import net.dpml.library.Type;
030    import net.dpml.library.Resource;
031    import net.dpml.library.info.Scope;
032    
033    import net.dpml.tools.Context;
034    
035    import net.dpml.transit.Transit;
036    
037    import org.apache.tools.ant.BuildException;
038    import org.apache.tools.ant.Project;
039    import org.apache.tools.ant.taskdefs.Exit;
040    import org.apache.tools.ant.taskdefs.optional.junit.BatchTest;
041    import org.apache.tools.ant.taskdefs.optional.junit.FormatterElement;
042    import org.apache.tools.ant.taskdefs.optional.junit.JUnitTask;
043    import org.apache.tools.ant.types.Environment;
044    import org.apache.tools.ant.types.FileSet;
045    import org.apache.tools.ant.types.Path;
046    import org.apache.tools.ant.types.Commandline;
047    
048    /**
049     * JUnit test execution.
050     *
051     * @author <a href="http://www.dpml.net">Digital Product Meta Library</a>
052     * @version 1.1.0
053     */
054    public class JUnitTestTask extends GenericTask
055    {
056       /**
057        * Constant for lookup of mx value.
058        */
059        public static final String MX_KEY = "project.test.mx";
060    
061       /**
062        * Constant test enabled key.
063        */
064        public static final String TEST_ENABLED_KEY = "project.test.enabled";
065    
066       /**
067        * Constant test src directory key.
068        */
069        public static final String TEST_SRC_KEY = "project.test.src";
070    
071       /**
072        * Constant test env directory key.
073        */
074        public static final String TEST_ENV_KEY = "project.test.env";
075    
076       /**
077        * Constant test debug key.
078        */
079        public static final String DEBUG_KEY = "project.test.debug";
080    
081       /**
082        * Constant test fork key.
083        */
084        public static final String FORK_KEY = "project.test.fork";
085    
086       /**
087        * Constant test fork mode key.
088        */
089        public static final String TEST_FORK_MODE_KEY = "project.test.fork.mode";
090    
091       /**
092        * Constant test halt-on-error key (default is false).
093        */
094        public static final String HALT_ON_ERROR_KEY = "project.test.halt-on-error";
095    
096       /**
097        * Constant test halt-on-failure key (default is false).
098        */
099        public static final String HALT_ON_FAILURE_KEY = "project.test.halt-on-failure";
100    
101       /**
102        * Constant test abort on error key - if true (the default) the build will fail
103        * if a test error occurs.
104        */
105        public static final String ABORT_ON_ERROR_KEY = "project.test.exit-on-error";
106    
107       /**
108        * Constant test abort on failure key - if true (the default) the build will fail
109        * if a test failure occurs.
110        */
111        public static final String ABORT_ON_FAILURE_KEY = "project.test.exit-on-failure";
112    
113       /**
114        * Constant cache path key.
115        */
116        public static final String CACHE_PATH_KEY = "dpml.cache";
117    
118       /**
119        * Constant work dir key.
120        */
121        public static final String WORK_DIR_KEY = "project.test.dir";
122    
123        /**
124        * the key for the include pattern for test cases
125        */
126        public static final String TEST_INCLUDES_KEY = "project.test.includes";
127    
128        /**
129        * default value
130        */
131        public static final String TEST_INCLUDES_VALUE = "**/*TestCase.java, **/*Test.java";
132    
133        /**
134        * the key for the exclude pattern for test cases
135        */
136        public static final String TEST_EXCLUDES_KEY = "project.test.excludes";
137    
138       /**
139        * default value
140        */
141        public static final String TEST_EXCLUDES_VALUE = "**/Abstract*.java, **/AllTest*.java";
142    
143       /**
144        * the key for the exclude pattern for test cases
145        */
146        public static final String VERBOSE_KEY = "project.test.verbose";
147    
148        private static final String ERROR_KEY = "project.test.error";
149        private static final String FAILURE_KEY = "project.test.failure";
150        private static final String TEST_SRC_VALUE = "test";
151        private static final String TEST_ENV_VALUE = "env";
152        private static final boolean DEBUG_VALUE = true;
153        private static final boolean FORK_VALUE = true;
154        private static final boolean HALT_ON_ERROR_VALUE = false;
155        private static final boolean HALT_ON_FAILURE_VALUE = false;
156    
157        private File m_source;
158        private String m_classPathRef;
159        private Path m_classPath;
160        
161       /**
162        * Task initialization.
163        * @exception BuildException if a build error occurs.
164        */
165        public void init() throws BuildException
166        {
167            if( !isInitialized() )
168            {
169                super.init();
170                final Project project = getProject();
171                project.setNewProperty( DEBUG_KEY, "" + DEBUG_VALUE );
172                project.setNewProperty( FORK_KEY, "" + FORK_VALUE );
173                project.setNewProperty( TEST_SRC_KEY, "" + TEST_SRC_VALUE );
174                project.setNewProperty( TEST_ENV_KEY, "" + TEST_ENV_VALUE );
175                project.setNewProperty( HALT_ON_ERROR_KEY, "" + HALT_ON_ERROR_VALUE );
176                project.setNewProperty( HALT_ON_FAILURE_KEY, "" + HALT_ON_FAILURE_VALUE );
177                getContext().getPath( Scope.TEST );
178            }
179        }
180    
181       /**
182        * Set the id of the compilation classpath.
183        * @param id the classpath reference
184        */
185        public void setClasspathRef( String id ) 
186        {
187            m_classPathRef = id;
188        }
189    
190       /**
191        * Set the classpath.
192        * @param path the classpath
193        */
194        public void setClasspath( Path path ) 
195        {
196            m_classPath = path;
197        }
198    
199       /**
200        * Set the directory containing the unit test source files.
201        * @param source the test source directory
202        */
203        public void setSrc( File source )
204        {
205            m_source = source;
206        }
207        
208        private Path getClasspath()
209        {
210            if( null != m_classPath )
211            {
212                return m_classPath;
213            }
214            else if( null != m_classPathRef )
215            {
216                return (Path) getProject().getReference( m_classPathRef );
217            }
218            else
219            {
220                final String error = 
221                  "Missing classpathref or classpath argument.";
222                throw new BuildException( error, getLocation() );
223            }
224        }
225        
226       /**
227        * Task execution.
228        * @exception BuildException if a build error occurs.
229        */
230        public void execute() throws BuildException
231        {
232            if( !isTestingEnabled() )
233            {
234                return;
235            }
236            
237            final Context context = getContext();
238            final Project project = getProject();
239            final File src = context.getTargetBuildTestDirectory();
240            if( src.exists() )
241            {
242                final File working = context.getTargetTestDirectory();
243                final Path classpath = getClasspath();
244                
245                executeUnitTests( src, classpath, working );
246                
247                if( getBooleanProperty( ABORT_ON_ERROR_KEY, true ) )
248                {
249                    final String error = project.getProperty( ERROR_KEY );
250                    if( null != error )
251                    {
252                        final String message =
253                            "One or more unit test errors occured.";
254                        fail( message );
255                    }
256                }
257                
258                if( getBooleanProperty( ABORT_ON_FAILURE_KEY, true ) )
259                {
260                    final String failure = project.getProperty( FAILURE_KEY );
261                    if( null != failure )
262                    {
263                        final String message =
264                            "One or more unit test failures occured.";
265                        fail( message );
266                    }
267                }
268            }
269        }
270        
271        private void executeUnitTests( final File src, final Path classpath, File working )
272        {
273            final Project project = getProject();
274            log( "Test classpath: " + classpath, Project.MSG_VERBOSE );
275            final FileSet fileset = createFileSet( src );
276            final JUnitTask junit = (JUnitTask) project.createTask( "junit" );
277            junit.setTaskName( getTaskName() );
278            
279            final JUnitTask.SummaryAttribute summary = getSummaryAttribute();
280            junit.setPrintsummary( summary );
281            
282            junit.setShowOutput( true );
283            junit.setTempdir( working );
284            junit.setReloading( true );
285            junit.setFiltertrace( true );
286            
287            junit.createClasspath().add( classpath );
288            
289            Context context = getContext();
290            String verbose = getVerboseArgument();
291            if( null != verbose )
292            {
293                Commandline.Argument arg = junit.createJvmarg();
294                arg.setValue( "-verbose:" + verbose );
295            }
296                
297            final File reports = getContext().getTargetReportsTestDirectory();
298            mkDir( reports );
299    
300            final BatchTest batch = junit.createBatchTest();
301            batch.addFileSet( fileset );
302            batch.setTodir( reports );
303    
304            final FormatterElement plain = newConfiguredFormatter( "plain" );
305            junit.addFormatter( plain );
306            
307            final FormatterElement xml = newConfiguredFormatter( "xml" );
308            junit.addFormatter( xml );
309            
310            final Environment.Variable work = new Environment.Variable();
311            work.setKey( WORK_DIR_KEY );
312            work.setValue( working.toString() );
313            junit.addConfiguredSysproperty( work );
314    
315            final Environment.Variable testBaseDir = new Environment.Variable();
316            testBaseDir.setKey( "project.test.dir" );
317            testBaseDir.setValue( working.toString() );
318            junit.addConfiguredSysproperty( testBaseDir );
319            
320            final Environment.Variable targetDir = new Environment.Variable();
321            targetDir.setKey( "project.target.dir" );
322            targetDir.setValue( getContext().getTargetDirectory().toString() );
323            junit.addConfiguredSysproperty( targetDir );
324    
325            final Environment.Variable deliverablesDir = new Environment.Variable();
326            deliverablesDir.setKey( "project.target.deliverables.dir" );
327            deliverablesDir.setValue( getContext().getTargetDeliverablesDirectory().toString() );
328            junit.addConfiguredSysproperty( deliverablesDir );
329    
330            final Environment.Variable basedir = new Environment.Variable();
331            basedir.setKey( "basedir" );
332            basedir.setValue( project.getBaseDir().toString() );
333            junit.addConfiguredSysproperty( basedir );
334    
335            final Environment.Variable basedir2 = new Environment.Variable();
336            basedir2.setKey( "project.basedir" );
337            basedir2.setValue( project.getBaseDir().toString() );
338            junit.addConfiguredSysproperty( basedir2 );
339    
340            final Environment.Variable cache = new Environment.Variable();
341            cache.setKey( CACHE_PATH_KEY );
342            cache.setValue( getCachePath() );
343            junit.addConfiguredSysproperty( cache );
344    
345            final File policy = new File( working, "security.policy" );
346            if( policy.exists() )
347            {
348                final Environment.Variable security = new Environment.Variable();
349                security.setKey( "java.security.policy" );
350                security.setValue( policy.toString() );
351                junit.addConfiguredSysproperty( security );
352            }
353    
354            setupTestProperties( junit, project );
355            
356            final File logProperties = new File( working, "logging.properties" );
357            if( logProperties.exists() )
358            {
359                final Environment.Variable log = new Environment.Variable();
360                log.setKey( "dpml.logging.config" );
361                try
362                {
363                    log.setValue( logProperties.toURL().toString() );
364                }
365                catch( IOException e )
366                {
367                    final String error = 
368                      "Unexpected file to url error."
369                      + "\nFile: " + logProperties;
370                    throw new BuildException( error, e );
371                }
372                junit.addConfiguredSysproperty( log );
373            }
374            
375            String formatter = getResource().getProperty( "java.util.logging.config.class" );
376            if( null != formatter )
377            {
378                final Environment.Variable logging = new Environment.Variable();
379                logging.setKey( "java.util.logging.config.class" );
380                if( "dpml".equals( formatter ) )
381                {
382                    logging.setValue( "net.dpml.util.ConfigurationHandler" );
383                }
384                else
385                {
386                    logging.setValue( formatter );
387                }
388                junit.addConfiguredSysproperty( logging );
389            }
390            
391            final Environment.Variable endorsed = new Environment.Variable();
392            endorsed.setKey( "java.endorsed.dirs" );
393            endorsed.setValue( new File( Transit.DPML_SYSTEM, "lib/endorsed" ).getAbsolutePath() );
394            junit.addConfiguredSysproperty( endorsed );
395            
396            configureDeliverableSysProperties( junit );
397            configureForExecution( junit );
398            
399            junit.setErrorProperty( ERROR_KEY );
400            junit.setFailureProperty( FAILURE_KEY );
401            final boolean haltOnErrorPolicy = getHaltOnErrorPolicy();
402            if( haltOnErrorPolicy )
403            {
404                junit.setHaltonerror( true );
405            }
406            final boolean haltOnFailurePolicy = getHaltOnFailurePolicy();
407            if( haltOnFailurePolicy )
408            {
409                junit.setHaltonfailure( true );
410            }
411            
412            junit.init();
413            junit.execute();
414        }
415        
416        private void setupTestProperties( JUnitTask junit, Project project )
417        {
418            File base = project.getBaseDir();
419            final File properties = new File( base, "test.properties" );
420            if( properties.exists() )
421            {
422                Properties props = new Properties();
423                try
424                {
425                    InputStream input = new FileInputStream( properties );
426                    props.load( input );
427                    Enumeration enum = props.propertyNames();
428                    while( enum.hasMoreElements() )
429                    {
430                        String name = (String) enum.nextElement();
431                        final Environment.Variable v = new Environment.Variable();
432                        v.setKey( name );
433                        v.setValue( props.getProperty( name ) );
434                        junit.addConfiguredSysproperty( v );
435                    }
436                }
437                catch( IOException ioe )
438                {
439                    final String error = 
440                      "Unexpected IO error while reading " + properties.toString();
441                    throw new BuildException( error, ioe, getLocation() );
442                }
443            }
444        }
445        
446        private void configureForExecution( JUnitTask task )
447        {
448            if( getForkProperty() )
449            {
450                task.setFork( true );
451                Project project = getProject();
452                task.setDir( project.getBaseDir() );
453                JUnitTask.ForkMode mode = getForkMode();
454                if( null == mode )
455                {
456                    log( "Executing forked test." );
457                }
458                else
459                {
460                    log( "Executing forked test with mode: '" + mode + "'." );
461                    task.setForkMode( mode );
462                }
463                String mx = getContext().getProperty( MX_KEY );
464                if( null != mx )
465                {
466                    task.setMaxmemory( mx );
467                }
468            }
469            else
470            {
471                log( "executing in local jvm" );
472                JUnitTask.ForkMode mode = new JUnitTask.ForkMode( "once" );
473                task.setForkMode( mode );
474                task.setFork( false );
475            }
476        }
477        
478        private void configureDeliverableSysProperties( JUnitTask task )
479        {
480            try
481            {
482                Context context = getContext();
483                Resource resource = context.getResource();
484                Type[] types = context.getResource().getTypes();
485                for( int i=0; i<types.length; i++ )
486                {
487                    Type type = types[i];
488                    String id = type.getID();
489                    File file = context.getTargetDeliverable( id );
490                    String path = file.getCanonicalPath();
491                    final Environment.Variable variable = new Environment.Variable();
492                    variable.setKey( "project.deliverable." + id + ".path" );
493                    variable.setValue( path );
494                    task.addConfiguredSysproperty( variable );
495                }
496            }
497            catch( IOException ioe )
498            {
499                final String error = 
500                  "Unexpected IO error while building deliverable filename properties.";
501                throw new BuildException( error, ioe, getLocation() );
502            }
503        }
504    
505        private FileSet createFileSet( File src )
506        {
507            final FileSet fileset = new FileSet();
508            fileset.setDir( src );
509            addIncludes( fileset );
510            addExcludes( fileset );
511            return fileset;
512        }
513    
514        private void addIncludes( FileSet set )
515        {
516            String pattern = getTestIncludes();
517            log( "Test includes=" + pattern, Project.MSG_VERBOSE );
518            StringTokenizer tokenizer = new StringTokenizer( pattern, ", ", false );
519            while( tokenizer.hasMoreTokens() )
520            {
521                String item = tokenizer.nextToken();
522                set.createInclude().setName( item );
523            }
524        }
525    
526        private void addExcludes( FileSet set )
527        {
528            String pattern = getTestExcludes();
529            log( "Test excludes=" + pattern, Project.MSG_VERBOSE );
530            StringTokenizer tokenizer = new StringTokenizer( pattern, ", ", false );
531            while( tokenizer.hasMoreTokens() )
532            {
533                String item = tokenizer.nextToken();
534                set.createExclude().setName( item );
535            }
536        }
537    
538        private String getTestIncludes()
539        {
540            String includes = getContext().getProperty( TEST_INCLUDES_KEY );
541            if( null != includes )
542            {
543                return includes;
544            }
545            else
546            {
547                return TEST_INCLUDES_VALUE;
548            }
549        }
550    
551        private String getTestExcludes()
552        {
553            String excludes = getContext().getProperty( TEST_EXCLUDES_KEY );
554            if( null != excludes )
555            {
556                return excludes;
557            }
558            else
559            {
560                return TEST_EXCLUDES_VALUE;
561            }
562        }
563    
564        private String getCachePath()
565        {
566            final String value = getContext().getProperty( CACHE_PATH_KEY );
567            if( null != value )
568            {
569                return value;
570            }
571            else
572            {
573                File cache = (File) getProject().getReference( "dpml.cache" );
574                return cache.toString();
575            }
576        }
577    
578        private boolean getDebugProperty()
579        {
580            return getBooleanProperty( DEBUG_KEY, DEBUG_VALUE );
581        }
582    
583        private boolean getForkProperty()
584        {
585            return getBooleanProperty( FORK_KEY, FORK_VALUE );
586        }
587    
588        private JUnitTask.ForkMode getForkMode()
589        {
590            final String value = getContext().getProperty( TEST_FORK_MODE_KEY );
591            if( null == value )
592            {
593                return null;
594            }
595            else
596            {
597                return new JUnitTask.ForkMode( value );
598            }
599        }
600    
601        private boolean getBooleanProperty( final String key, final boolean fallback )
602        {
603            final String value = getContext().getProperty( key );
604            if( null == value )
605            {
606                return fallback;
607            }
608            else
609            {
610                return Project.toBoolean( value );
611            }
612        }
613    
614        private void fail( final String message )
615        {
616            final Exit exit = (Exit) getProject().createTask( "fail" );
617            exit.setMessage( message );
618            exit.init();
619            exit.execute();
620        }
621    
622        private boolean isTestingEnabled()
623        {
624            final String enabled = getContext().getProperty( TEST_ENABLED_KEY, "true" );
625            return "true".equals( enabled );
626        }
627    
628        private JUnitTask.SummaryAttribute getSummaryAttribute()
629        {
630            final Project project = getProject();
631            final JUnitTask.SummaryAttribute summary = new JUnitTask.SummaryAttribute();
632            summary.setValue( "on" );
633            return summary;
634        }
635        
636        private boolean getHaltOnErrorPolicy()
637        {
638            return getBooleanProperty(
639                HALT_ON_ERROR_KEY, HALT_ON_ERROR_VALUE );
640        }
641        
642        private boolean getHaltOnFailurePolicy()
643        {
644            return getBooleanProperty(
645                HALT_ON_FAILURE_KEY, HALT_ON_FAILURE_VALUE );
646        }
647        
648        private String getVerboseArgument()
649        {
650            Context context = getContext();
651            return context.getProperty( "project.test.verbose" );
652        }
653        
654        private FormatterElement newConfiguredFormatter( String type )
655        {
656            final FormatterElement formatter = new FormatterElement();
657            final FormatterElement.TypeAttribute attribute = new FormatterElement.TypeAttribute();
658            attribute.setValue( type );
659            formatter.setType( attribute );
660            return formatter;
661        }
662        
663    }